Python VS R:双语数据分析——上海租房数据全解析
作者:董天一
个人公众号:生物与化学数据分析
房价看多了一把泪,年轻人还是多看看租房信息好了,毕竟日子还是要过滴。
由于周围的朋友很多都选择在上海发展,所以这次我把目标锁定在了上海,看看上海的租房行情怎么样。(温馨提示:看之前要调整好心态。。)
本文使用python和R两种语言进行数据分析,尽量做到代码完整,易于理解。关注的重点在于数据清洗与分析的流程,每一部分的代码不一定是实现完全一样的效果,在对比之中实现取长补短、各显其能才是最佳的做法。感兴趣的同学们赶紧来一起练手吧!
一、数据获取和读入
使用python编写爬虫,爬取了链家网上海3W+的租房信息,包括价格、面积、朝向、户型等数据。
在python中使用pandas读入数据;在R中使用readxl包读入数据。
python
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
data = pd.read_excel("链家网上海租房信息汇总.xlsx")
data.head()
R
library(readxl)
data <- read_xlsx("链家网上海租房信息汇总.xlsx")
data <- data[, -1]
head(data)
二、数据概况
1、数据类型
python
R
可以看到,python和R读数据的时候还是有些差别的,python将纯数字的kanguo和price两列读成了整型,而R则将所有变量都读成了字符串,所以之后在做预处理的时候同样也要区别处理。
2、缺失值查看
python
data.apply(lambda x : sum(x.isnull()))
R
library(VIM)
table(is.na(data))
aggr(data, prop = FALSE, numbers = TRUE)
python中使用apply结合lambda表达式进行缺失值查看,是常用的做法;而R中则有比较多的方法,这里采用了is.na()函数进行缺失值的全局查看,并使用VIM包的aggr函数进行缺失值的可视化。无论用哪一种方法,我们所有的数据都没有观测到有缺失值,这首先要归功于链家网数据的完整性,当然更重要的,要归功于我们的爬虫写的太好了。
3、赶紧看看价格呀!
对滴对滴,拿到数据赶紧看看最重要的——价格都是多钱呀!
python
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
data["price"].plot(kind = "box", fontsize = 15)
plt.ylim(0, 50000)
plt.title("上海租房价格分布箱线图")
plt.show()
R
library(psych)
data$price <- as.numeric(data$price)
describe(data$price)
这里我们利用python的pandas快捷作图作出价格分布的箱线图,用R的psych包对租房价格做一下基本的描述性统计。
从箱线图中看出一半的出租房价格集中在4000-10000的范围内,中位数为5500。
4、哎哟,一个月几十万的租金?
是呀兄弟们,我也看见了,上面统计的时候,最高的租金是每个月58W?
此时按捺不住好奇心的我取出了每月租金高于10W的子集。
python
data[data["price"] > 100000]
R
data[data$price > 100000, ]
居然有25套房子每个月要10W以上的租金!我们随便找一个房子的信息百度一下看看到底是啥样的房子。。对!就找那个最贵的58W的!凤阳路338号!9室9厅,1338平,我滴妈???
打开网页的我是崩溃的。
古董洋房我去!一个月才58W呢,真是白菜价哟!
三、数据清洗和单一变量分析
1、供水区和楼层
ceng的一列中包含了两部分信息,供水区和楼层,我们它拆成两个变量。(低区、中区、高区指的原来是不同供水方式的区域,我也是百度到的,真是涨姿势呀。)
python
import re
a = r"(.*?)区"
b = r"\d+"
def get_water_supply_region(x):
if len(re.findall(a, x)) == 0:
return "无"
else:
return re.findall(a,x)[0]
data["ceng"] = data["ceng"].apply(lambda x : x.strip())
data["water_supply_region"] = data["ceng"].apply(get_water_supply_region)
data["floor"] = data["ceng"].apply(lambda x : re.findall(b, x)[0])
data["floor"] = data["floor"].astype(np.int64)
data.drop("ceng", axis = 1, inplace = True)
data["water_supply_region"].value_counts().plot(kind = "bar")
plt.xticks(rotation = 0)
plt.xlabel("供水区")
plt.ylabel("count")
plt.title("上海租房供水区分布柱状图")
plt.show()
R
library(stringr)
data$water_supply_region <- str_extract(data$ceng, "(.*?)区")
data$water_supply_region[is.na(data$water_supply_region)] <- "无"
data$floor <- str_extract(data$ceng, "[:digit:]+层")
data$floor <- str_extract(data$floor, "[:digit:]+")
data$floor <- as.numeric(data$floor)
data <- data[,-1]
table(data$water_supply_region)
使用正则表达式提取相应数据,python中使用re库,R中使用stringr包,都很方便,不过两种语言的正则表达式语法稍有不同,需要分别学习。
从柱状图中可以看出,高区和中区的房屋要多于地区的房屋,不愧是大魔都呀。
由于楼层信息较为零散,我们将其切段,0代表1-5层,1代表6-10层,2代表11-20层,3代表21-30层,4代表30层以上。
python
data["floor"] = data["floor"].apply(lambda x : 0 if (1 <= x <= 5) else x)
data["floor"] = data["floor"].apply(lambda x : 1 if (6 <= x <= 10) else x)
data["floor"] = data["floor"].apply(lambda x : 2 if (11 <= x <= 20) else x)
data["floor"] = data["floor"].apply(lambda x : 3 if (21 <= x <= 30) else x)
data["floor"] = data["floor"].apply(lambda x : 4 if (x > 30) else x)
aa = data["floor"].value_counts()
cc = pd.DataFrame(aa)
cc.columns = ["count"]
cc["floor"] = cc.index
cc = cc.sort_values(by = "floor")
plt.bar(cc["floor"], cc["count"])
plt.xticks(cc["floor"], ["1-5层", "6-10层", "11-20层", "21-30层", "30层以上"], rotation = 0)
plt.xlabel("楼层")
plt.ylabel("count")
plt.title("上海租房楼层分布柱状图")
plt.show()
R
data$floor2 <- data$floor
data$floor2[data$floor >= 1 & data$floor <= 5] <- "1-5层"
data$floor2[data$floor >= 6 & data$floor <= 10] <- "6-10层"
data$floor2[data$floor >= 11 & data$floor <= 20] <- "11-20层"
data$floor2[data$floor >= 21 & data$floor <= 30] <- "21-30层"
data$floor2[data$floor > 30] <- "30层以上"
library(sqldf)
library(ggplot2)
plot_set <- sqldf("select floor2, count(region) as counts from data group by floor2")
plot_set$floor2 <- factor(plot_set$floor2, levels = c("1-5层","6-10层", "11-20层", "21-30层", "30层以上"))
ggplot(plot_set, aes(x = floor2, y = counts)) + geom_bar(stat = "identity", width = .5) + theme(axis.title = element_text(size = 18), axis.text = element_text(size = 16))
从图中可以看出,上海出租房的楼层分布主要集中在6-10层,以及20层以内。
2、朝向
我们先看看朝向的取值都有哪些:
python
R
有将近一半的房屋没有朝向的数据(在爬取数据时就用“无朝向数据”填充了),其他的各种朝向都有,我们来可视化一下。
python
data["chaoxiang"].value_counts().plot(kind = "bar")
plt.xlabel("朝向")
plt.ylabel("count")
plt.title("上海租房朝向分布柱状图")
plt.show()
R
plot_set2 <- sqldf("select chaoxiang, count(region) as counts from data group by chaoxiang order by counts DESC")
ggplot(plot_set2, aes(x = chaoxiang, y = counts)) + geom_bar(stat = "identity", width = .5) + theme(axis.title = element_text(size = 18), axis.text = element_text(size = 16), axis.text.x = element_text(angle = 30))
在拥有朝向数据的房屋中,朝南和朝南北的占绝大多数。“朝南北”的房屋对大多数人更具有吸引力,中介也往往用“南北通透”来做宣传。
既然不朝南或者不是南北通透的房子都是“奇葩”,那我们干脆将其他的朝向都纳入“其他”了事。
python
qita = ["朝北", "朝东", "朝东北", "朝东南", "朝东西", "朝西", "朝西北", "朝西南"]
for each in qita:
data["chaoxiang"] = data["chaoxiang"].apply(lambda x : "其他" if x == each else x)
R
qita <- c("朝北", "朝东", "朝东北", "朝东南", "朝东西", "朝西", "朝西北", "朝西南")
for(i in qita){data$chaoxiang[data$chaoxiang == i] <- "其他"}
3、发布日期
发布日期不是我们重点关注的点,这里我们把发布日期的字符串转为时间序列,做一下可视化,然后丢掉之。
python
data["date"] = data["date"].apply(lambda x : x[: 10])
data["date"] = pd.to_datetime(data["date"])
data["date"].value_counts().plot()
plt.xlabel("日期")
plt.ylabel("count")
plt.title("上海租房发布数量走势")
plt.show()
data.drop("date", axis = 1, inplace = True)
R
data$date <- substr(data$date, 1, 10)
data$date <- as.Date(data$date, "%Y.%m.%d")
plot_set3 <- sqldf("select date, count(region) as counts from data group by date order by date")
plot(plot_set3$date, plot_set3$counts, "l")
data <- data[, -2]
ok,看到从今年三月份开始,链家网上海的租房信息才开始逐渐增多的。
4、带看次数
带看次数的取值也比较多,我们也采取切段的方式处理:0代表0次,1代表1次,2代表2-5次,3代表6-10次,4代表10次以上。
python
data["kanguo"] = data["kanguo"].apply(lambda x : 2 if (2 <= x <= 5) else x)
data["kanguo"] = data["kanguo"].apply(lambda x : 3 if (6 <= x <= 10) else x)
data["kanguo"] = data["kanguo"].apply(lambda x : 4 if (x > 10) else x)
data["kanguo"].value_counts().plot(kind = "bar")
plt.xlabel("带看次数")
plt.ylabel("count")
plt.title("上海租房带看次数分布柱状图")
plt.xticks([0, 1, 2, 3, 4], ["0", "2-5", "1", "6-10", "> 10"], rotation = 0)
plt.show()
R
data$kanguo <- as.numeric(data$kanguo)
data$kanguo2 <- data$kanguo
data$kanguo2[data$kanguo == 0] <- "0次"
data$kanguo2[data$kanguo == 1] <- "1次"
data$kanguo >= 2 & data$kanguo <= 5] <- "2-5次"
data$kanguo2[data$kanguo >= 6 & data$kanguo <= 10] <- "6-10次"
data$kanguo2[data$kanguo > 10] <- "多于10次"
plot_set4 <- sqldf("select kanguo2, count(region) as counts from data group by kanguo2")
ggplot(plot_set4, aes(x = kanguo2, y = counts)) + geom_bar(stat = "identity", width = .5) + theme(axis.title = element_text(size = 18), axis.text = element_text(size = 16))
有三分之一多的房屋还静静地躺着,并没有人去看过;看过6次以上的抢手出租房有约5000套,不过看了这么多次也有可能是大坑,一定要小心选择啊。
5、面积
面积数据在爬取的时候周围有换行符等等干扰,所以要清理一下,再转成整型。这里依然使用正则表达式提取关键数据。
python
a = r"(.*?)平"
data["mianji"] = data["mianji"].apply(lambda x : re.findall(a, x)[0])
data["mianji"] = data["mianji"].astype(np.int64)
R
data$mianji <- str_extract(data$mianji, "(.*?)平")
data$mianji <- str_extract(data$mianji, "[:digit:]+")
6、区
各区的数据比较完整,取值也不多,来看看各区房屋数量多少吧。
python
data["region"].value_counts().plot(kind = "barh")
plt.xlabel("count")
plt.title("上海各区租房数量分布条形图")
plt.show()
R
plot_set5 <- sqldf("select region, count(chaoxiang) as counts from data group by region order by counts DESC")
ggplot(plot_set5, aes(x = region, y = counts)) + geom_bar(stat = "identity", width = .5) + theme(axis.title = element_text(size = 18), axis.text = element_text(size = 16)) + coord_flip()
浦东出租房数量遥遥领先于其他区,闵行、徐汇和宝山位于2-4位,最少的崇明在链家网上只有7套房屋在出租。
7、户型
户型的数据格式为X室X厅,我们先对各种户型出租房的数量进行可视化分析,然后将其拆分为rooms和living_rooms两列。
python
data["shiting"] = data["shiting"].apply(lambda x : x.strip())
aa = data["shiting"].value_counts()
#将计数为500以下的户型都纳入“其他”
qita = aa[: 10].sum()
aa = aa[: 10]
aa["其他"] = qita
cc = pd.DataFrame(aa)
cc["huxing"] = cc.index
fig, (ax, ax2) = plt.subplots(2, 1, sharex=True)
ax.bar(range(1, 12), cc["shiting"])
ax2.bar(range(1, 12), cc["shiting"])
ax.set_ylim(28000, 30000)
ax2.set_ylim(0, 10000)
ax.spines["bottom"].set_visible(False)
ax2.spines["top"].set_visible(False)
ax.xaxis.tick_top()
ax.tick_params(labeltop = "off")
ax2.xaxis.tick_bottom()
plt.xticks(range(1, 12), list(cc["huxing"]), rotation = 60)
#绘制坐标轴截断线
d = .015
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((-d, +d), (-d, +d), **kwargs)
ax.plot((1 - d, 1 + d), (-d, +d), **kwargs)
kwargs.update(transform=ax2.transAxes)
ax2.plot((-d, +d), (1 - d, 1 + d), **kwargs)
ax2.plot((1 - d, 1 + d), (1 - d, 1 + d), **kwargs)
plt.xlabel("户型")
ax.set_title("上海租房户型分布柱状图")
ax.set_ylabel("count", position = (0, 0))
plt.show()
#拆分
data["rooms"] = data["shiting"].apply(lambda x : re.findall(r"(.*?)室", x)[0])
data["living_rooms"] = data["shiting"].apply(lambda x : re.findall(r"室(.*?)厅", x)[0])
data["rooms"] = data["rooms"].astype(np.int64)
data["living_rooms"] = data["living_rooms"].astype(np.int64)
data.drop("shiting", axis = 1, inplace = True)
这里将数量较少的户型并入“其他”,然后采用截断式柱状图进行可视化。
户型种类繁多,尤其还包括上面“超级洋房”9室9厅那种。不过最多的还是2室2厅,3室2厅,2室1厅这样的家庭、合租式的住房以及1室1厅这样的单身出租房。
使用R作截断式柱状图比较繁琐,下面只给出拆分列的代码,可视化代码略去
R
data$rooms <- str_extract(data$shiting, "[:digit:]+室")
data$living_rooms <- str_extract(data$shiting, "室[:digit:]+厅")
data$rooms <- str_extract(data$rooms, "[:digit:]+")
data$living_rooms <- str_extract(data$living_rooms, "[:digit:]+")
data <- data[, -6]
8、街道、小区、标题
街道、小区和标题信息太过杂乱,而数据量又并不大,所以选择去掉。
python
data.drop(["street", "xiaoqu", "title"], axis = 1, inplace = True)
R
data <- data[, c(-6, -7, -8)]
9、变量类型和其他处理
对于python中的数据框,带看数和层数在切段后要转为字符串型;对于R中的数据框,切段前的带看数和层数需要去掉,几列数值型变量需要转换,直接看代码。
python
data["kanguo"] = data["kanguo"].astype(str)
data["floor"] = data["floor"].astype(str)
data.head()
R
data <- data[, c(-2, -7)]
data$mianji <- as.numeric(data$mianji)
data$rooms <- as.numeric(data$rooms)
data$living_rooms <- as.numeric(data$living_rooms)
head(data)
四、多变量分析
通过多变量的分析,可以实现一些直观的想法,比如不同区内房屋价格的分布情况是怎样的?
python
sns.boxplot(x = "price", y = "region", data = data)
plt.title("上海各区租房价格分布箱线图")
plt.xlim(0, 40000)
plt.show()
R
ggplot(data, aes(x = region, y = price, fill = region)) + geom_boxplot(size = 1) + coord_flip() + ylim(0, 40000) + scale_fill_hue()
从箱线图中看出,静安的租房价格最高,百度了一下,果然,静安区是市中心呀。
接下来可以看看各区租房面积的分布是怎样的。
python
sns.boxplot(x = "mianji", y = "region", data = data)
plt.title("上海各区租房面积分布箱线图")
plt.xlim(0, 1000)
plt.show()
R
ggplot(data, aes(x = region, y = mianji, fill = region)) + geom_boxplot(size = 1) + coord_flip() + ylim(0, 1000) + scale_fill_hue()
好吧,面积分布都差不多,青浦的出租房面积稍大一些。
和买房一样,租房也要看单价的呀,我们再来看下各区房屋单价的分布吧。
python
data["unit_price"] = data["price"] / data["mianji"]
sns.boxplot(x = "unit_price", y = "region", data = data)
plt.title("上海各区租房单价分布箱线图")
plt.xlim(0, 600)
plt.show()
R
data$unit_price <- data$price / data$mianji
ggplot(data, aes(x = region, y = unit_price, fill = region)) + geom_boxplot(size = 1) + coord_flip() + ylim(0, 600) + scale_fill_hue()
看起来还是静安价格最高,不过高低排名看得不是很清晰。我们把各区单价的中位数提取出来作漏斗图,就清楚多了:
python
aa = data["unit_price"].groupby(data["region"]).median().sort_values(ascending = True)
cc = pd.DataFrame(aa)
cc["region"] = cc.index
cc["place_holder"] = (150 - cc["unit_price"]) / 2
fig, ax = plt.subplots()
ax.barh(range(len(cc)), cc["unit_price"], align = "center", left = cc["place_holder"])
plt.yticks(range(len(cc)), cc["region"], rotation = 0)
labels = ["%.2f"%each for each in cc["unit_price"]]
for i in range(len(cc.iloc[4:, :])):
ax.text(x = 69, y = [k for k in range(len(cc))][i] - .2, s = labels[i], color = "black", size = 10)
for i in range(len(cc.iloc[0:4, :])):
ax.text(x = 67.3, y = [14,15,16,17][i] - .2, s = labels[i + 14], color = "black", size = 10)
plt.xlabel("unit_price")
plt.title("上海各区租房单价中位数漏斗图")
plt.show()
R
plot_set6 <- aggregate(data$unit_price, by = list(data$region), median)
names(plot_set6) <- c("region", "price_median")
plot_set6 <- sqldf("select * from plot_set6 order by price_median")
plot_set6$price_median <- round(plot_set6$price_median, 2)
plot_set6$placeholder <- round((150 - plot_set6$price_median) / 2, 2)
new_plot_set6 <- melt(plot_set6, id = "region")
new_plot_set6$region <- factor(new_plot_set6$region, levels = plot_set6$region)
new_plot_set6$variable <- factor(new_plot_set6$variable, levels = c("price_median", "placeholder"))
p1 <- ggplot(new_plot_set6) + geom_bar(aes(x = region, y = value, fill = variable), stat = "identity")
p2 <- p1 + coord_flip() + guides(fill = F) + theme(panel.background = element_blank())
p3 <- p2 + scale_fill_manual(values = c("steelblue2", "white"))
p4 <- p3 + annotate("text", x = 1:18, y = 75, label = plot_set6$price_median)
p4
用漏斗图来看就清楚多了,租房单价最高的是静安区,平均每平米要将近150块;接着是黄浦、长宁和徐汇;房屋出租数量最多的浦东价格在中间的位置,平均每平米78块多,听起来还可以的样子。而在崇明,租一个一百平的房子一个月只要两千多(虽然数据量太少),真是适宜生存呢。
接着我们再来看一下,是不是真的“南北通透”的房子价格会高一些呢?
因为奇葩的价格还是比较多的,我们还是用带有中位数线的箱线图来看。
python
sns.boxplot(x = "chaoxiang", y = "price", data = data)
plt.xlabel("朝向")
plt.ylabel("价格")
plt.title("上海租房各朝向价格分布箱线图")
plt.ylim(0,50000)
plt.show()
R
ggplot(data, aes(x = chaoxiang, y = price)) + geom_boxplot() + ylim(0, 50000)
从箱线图中可以看出,“朝南北”的房屋价格是要高一些。
五、房价预测
最后再来一波简单的房价预测吧。我们用python实现随机森林算法,用于预测房价。
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score
new_data = pd.get_dummies(data)
X = new_data.drop("price", axis = 1).values
y = new_data["price"].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state = 71)
rf = RandomForestRegressor(n_estimators = 1000, n_jobs = 7, max_depth = 10, random_state = 71)
rf.fit(X_train, y_train)
y_train_pred = rf.predict(X_train)
y_test_pred = rf.predict(X_test)
print("R^2 train: %.3f, test: %.3f" % (r2_score(y_train, y_train_pred), r2_score(y_test, y_test_pred)))
可以看到效果还可以,但是在测试集上的结果出现了过拟合,需要进一步的参数优化。
ok,上海租房情况就分析到这里,魔都的租房行情已经了解的差不多了。数据不多,但分析的过程比较长,耐心看完,一起操作,一定会有不小的提高!
文末扫码关注Python爱好者社区,后台回复‘上海租房’即可获得:
本文爬虫源代码.py和爬取数据汇总 + 数据分析流程Ipython notebook + 数据分析流程R Script
Python爱好者社区历史文章大合集:
Python爱好者社区历史文章列表(每周append更新一次)
关注后在公众号内回复“课程”即可获取:
1.崔老师爬虫实战案例免费学习视频。
2.丘老师数据科学入门指导免费学习视频。
3.陈老师数据分析报告制作免费学习视频。
4.玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。
5.丘老师Python网络爬虫实战免费学习视频。